// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include <zencore/zencore.h>

#include <string_view>

ZEN_THIRD_PARTY_INCLUDES_START
#include <spdlog/formatter.h>
#include <spdlog/pattern_formatter.h>
ZEN_THIRD_PARTY_INCLUDES_END

namespace zen::logging {

class full_formatter final : public spdlog::formatter
{
public:
	full_formatter(std::string_view LogId, std::chrono::time_point<std::chrono::system_clock> Epoch)
	: m_Epoch(Epoch)
	, m_LogId(LogId)
	, m_LinePrefix(128, ' ')
	, m_UseFullDate(false)
	{
	}

	full_formatter(std::string_view LogId) : m_LogId(LogId), m_LinePrefix(128, ' '), m_UseFullDate(true) {}

	virtual std::unique_ptr<formatter> clone() const override { return std::make_unique<full_formatter>(m_LogId, m_Epoch); }

	virtual void format(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& OutBuffer) override
	{
		// Note that the sink is responsible for ensuring there is only ever a
		// single caller in here

		using namespace std::literals;

		std::chrono::seconds TimestampSeconds;

		if (m_UseFullDate)
		{
			TimestampSeconds = std::chrono::duration_cast<std::chrono::seconds>(msg.time.time_since_epoch());
			if (TimestampSeconds != m_LastLogSecs)
			{
				RwLock::ExclusiveLockScope _(m_TimestampLock);
				m_LastLogSecs = TimestampSeconds;

				m_CachedLocalTm = spdlog::details::os::localtime(spdlog::log_clock::to_time_t(msg.time));
				m_CachedDatetime.clear();
				m_CachedDatetime.push_back('[');
				spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_year % 100, m_CachedDatetime);
				m_CachedDatetime.push_back('-');
				spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_mon + 1, m_CachedDatetime);
				m_CachedDatetime.push_back('-');
				spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_mday, m_CachedDatetime);
				m_CachedDatetime.push_back(' ');
				spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_hour, m_CachedDatetime);
				m_CachedDatetime.push_back(':');
				spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_min, m_CachedDatetime);
				m_CachedDatetime.push_back(':');
				spdlog::details::fmt_helper::pad2(m_CachedLocalTm.tm_sec, m_CachedDatetime);
				m_CachedDatetime.push_back('.');
			}
		}
		else
		{
			auto ElapsedTime = msg.time - m_Epoch;
			TimestampSeconds = std::chrono::duration_cast<std::chrono::seconds>(ElapsedTime);

			if (m_CacheTimestamp.load() != TimestampSeconds)
			{
				RwLock::ExclusiveLockScope _(m_TimestampLock);

				m_CacheTimestamp = TimestampSeconds;

				int		  Count	  = int(TimestampSeconds.count());
				const int LogSecs = Count % 60;
				Count /= 60;
				const int LogMins = Count % 60;
				Count /= 60;
				const int LogHours = Count;

				m_CachedDatetime.clear();
				m_CachedDatetime.push_back('[');
				spdlog::details::fmt_helper::pad2(LogHours, m_CachedDatetime);
				m_CachedDatetime.push_back(':');
				spdlog::details::fmt_helper::pad2(LogMins, m_CachedDatetime);
				m_CachedDatetime.push_back(':');
				spdlog::details::fmt_helper::pad2(LogSecs, m_CachedDatetime);
				m_CachedDatetime.push_back('.');
			}
		}

		{
			RwLock::SharedLockScope _(m_TimestampLock);
			OutBuffer.append(m_CachedDatetime.begin(), m_CachedDatetime.end());
		}

		auto millis = spdlog::details::fmt_helper::time_fraction<std::chrono::milliseconds>(msg.time);
		spdlog::details::fmt_helper::pad3(static_cast<uint32_t>(millis.count()), OutBuffer);
		OutBuffer.push_back(']');
		OutBuffer.push_back(' ');

		if (!m_LogId.empty())
		{
			OutBuffer.push_back('[');
			spdlog::details::fmt_helper::append_string_view(m_LogId, OutBuffer);
			OutBuffer.push_back(']');
			OutBuffer.push_back(' ');
		}

		// append logger name if exists
		if (msg.logger_name.size() > 0)
		{
			OutBuffer.push_back('[');
			spdlog::details::fmt_helper::append_string_view(msg.logger_name, OutBuffer);
			OutBuffer.push_back(']');
			OutBuffer.push_back(' ');
		}

		OutBuffer.push_back('[');
		// wrap the level name with color
		msg.color_range_start = OutBuffer.size();
		spdlog::details::fmt_helper::append_string_view(spdlog::level::to_string_view(msg.level), OutBuffer);
		msg.color_range_end = OutBuffer.size();
		OutBuffer.push_back(']');
		OutBuffer.push_back(' ');

		// add source location if present
		if (!msg.source.empty())
		{
			OutBuffer.push_back('[');
			const char* filename =
				spdlog::details::short_filename_formatter<spdlog::details::null_scoped_padder>::basename(msg.source.filename);
			spdlog::details::fmt_helper::append_string_view(filename, OutBuffer);
			OutBuffer.push_back(':');
			spdlog::details::fmt_helper::append_int(msg.source.line, OutBuffer);
			OutBuffer.push_back(']');
			OutBuffer.push_back(' ');
		}

		// Handle newlines in single log call by prefixing each additional line to make
		// subsequent lines align with the first line in the message

		const size_t LinePrefixCount = Min<size_t>(OutBuffer.size(), m_LinePrefix.size());

		auto ItLineBegin  = msg.payload.begin();
		auto ItMessageEnd = msg.payload.end();
		bool IsFirstline  = true;

		{
			auto ItLineEnd = ItLineBegin;

			auto EmitLine = [&] {
				if (IsFirstline)
				{
					IsFirstline = false;
				}
				else
				{
					spdlog::details::fmt_helper::append_string_view(std::string_view(m_LinePrefix.data(), LinePrefixCount), OutBuffer);
				}
				spdlog::details::fmt_helper::append_string_view(spdlog::string_view_t(&*ItLineBegin, ItLineEnd - ItLineBegin), OutBuffer);
			};

			while (ItLineEnd != ItMessageEnd)
			{
				if (*ItLineEnd++ == '\n')
				{
					EmitLine();
					ItLineBegin = ItLineEnd;
				}
			}

			if (ItLineBegin != ItMessageEnd)
			{
				EmitLine();
				spdlog::details::fmt_helper::append_string_view("\n"sv, OutBuffer);
			}
		}
	}

private:
	std::chrono::time_point<std::chrono::system_clock> m_Epoch;
	std::tm											   m_CachedLocalTm;
	std::chrono::seconds							   m_LastLogSecs;
	std::atomic<std::chrono::seconds>				   m_CacheTimestamp;
	spdlog::memory_buf_t							   m_CachedDatetime;
	std::string										   m_LogId;
	std::string										   m_LinePrefix;
	bool											   m_UseFullDate = true;
	RwLock											   m_TimestampLock;
};

}  // namespace zen::logging
